package org.sean.framework.util;

import cn.hutool.extra.emoji.EmojiUtil;
import org.sean.framework.code.StatusInfo;
import org.sean.framework.exception.StatusException;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * String 工具类
 */
public class StringUtil {
    private static final int INDEX_NOT_FOUND = -1;

    private static final String EMPTY = "";
    private static final String SIMPLE_PHONE = "^[1][0-9]{10}$";
    private static final String SYMBOL = "[`~!@#$%^&*()+=|{}':;,\\[\\].<>/?～！＠＃＄％＾＆＊（）￥…—【】‘；：”“’。，、？]";
    private static final String EMAIL = "^\\w+([-_.]?\\w{1,50})*@\\w{1,50}([.-]?\\w{1,50})*(\\.\\w{2,10})+$";

    private StringUtil() {
    }

    /**
     * 把控字符转换成空串
     *
     * @param value 原始字符串
     * @return 处理后字符串
     */
    public static String transNull(String value) {
        if (value != null) {
            return value;
        } else {
            return "";
        }
    }

    /**
     * 把字符串根据分隔符生成数组。
     *
     * @param line  字符串
     * @param delim 分隔符
     * @return 分割数据
     */
    public static String[] split(String line, String delim) {
        if (line == null) {
            return new String[0];
        }
        List<String> list = new ArrayList<>();
        StringTokenizer t = new StringTokenizer(line, delim);

        while (t.hasMoreTokens()) {
            list.add(t.nextToken());
        }

        return list.toArray(new String[0]);
    }

    /**
     * 字符串是否为空
     *
     * @param value value
     * @return 是否不为空
     */
    public static boolean isEmpty(CharSequence value) {
        if (value instanceof String) {
            return ((String) value).trim().length() == 0;
        } else {
            return value == null || value.length() == 0;
        }
    }

    /**
     * map 是否不为空
     *
     * @param value value
     * @return 是否不为空
     */
    public static boolean isNotEmpty(String value) {
        return !isEmpty(value);
    }

    /**
     * 判断是否相同,不区分大小写
     *
     * @param v1 v1
     * @param v2 v2
     * @return 是否相同
     */
    public static boolean equalsIgnoreCase(String v1, String v2) {
        if (v1 == null || v2 == null) {
            return false;
        }
        return v1.equalsIgnoreCase(v2);
    }

    /**
     * 判断是否相同,区分大小写
     *
     * @param v1 v1
     * @param v2 v2
     * @return 是否相同
     */
    public static boolean equals(String v1, String v2) {
        if (v1 == null || v2 == null) {
            return false;
        }
        return v1.equals(v2);
    }

    /**
     * 是否为true
     *
     * @param value value
     * @return is true value
     */
    public static boolean isTrue(Boolean value) {
        return ObjectUtil.isTrue(value);
    }

    /**
     * 是否为true
     *
     * @param value value
     * @return is true value
     */
    public static boolean isTrue(String value) {
        return isTrue(Boolean.valueOf(value));
    }

    /**
     * <p>Checks if a CharSequence is whitespace, empty ("") or null.</p>
     * <pre>
     * StringUtil.isBlank(null)      = true
     * StringUtil.isBlank("")        = true
     * StringUtil.isBlank(" ")       = true
     * StringUtil.isBlank("bob")     = false
     * StringUtil.isBlank("  bob  ") = false
     * </pre>
     *
     * @param cs the CharSequence to check, may be null
     * @return 是否为空串
     */
    public static boolean isBlank(CharSequence cs) {
        int strLen;
        if (cs == null || (strLen = cs.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if (!Character.isWhitespace(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * <p>Checks if a CharSequence is not empty (""), not null and not whitespace only.</p>
     * <pre>
     * StringUtil.isNotBlank(null)      = false
     * StringUtil.isNotBlank("")        = false
     * StringUtil.isNotBlank(" ")       = false
     * StringUtil.isNotBlank("bob")     = true
     * StringUtil.isNotBlank("  bob  ") = true
     * </pre>
     *
     * @param cs the CharSequence to check, may be null
     * @return 是否不为空串
     */
    public static boolean isNotBlank(CharSequence cs) {
        return !isBlank(cs);
    }


    /**
     * 格式化文本
     * <pre>
     * example:
     * formatContent("我是{0}","Sean")
     * result:  我是Sean
     *
     * </pre>
     * <p>
     * 包含单引号是 需使用两个单引号''{0}'' , 整数过千 转换是去掉逗号 {0,number,#}
     *
     * @param content content
     * @param args    参数
     * @return 格式化字符串
     */
    public static String formatContent(String content, Object... args) {
        if (args != null && args.length > 0) {
            return MessageFormat.format(content, args);
        } else {
            return content;
        }
    }

    /**
     * 是否为中文字符
     *
     * @param c 字符
     * @return 是否是中文字符
     */
    public static boolean isChinese(char c) {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
        return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS;

    }

    /**
     * 判断是否包含中文字符
     *
     * @param value 原始串
     * @return 是否包含中文字符
     */
    public static boolean containChinese(String value) {
        if (isEmpty(value)) {
            return false;
        }
        return value.chars().anyMatch(c -> isChinese((char) c));
    }

    /**
     * 判断是否全是中文字符
     *
     * @param value 原始串
     * @return 是否都是中文字符
     */
    public static boolean allChinese(String value) {
        if (isEmpty(value)) {
            return false;
        }
        return value.chars().allMatch(c -> isChinese((char) c));
    }

    /**
     * 手机号简单校验
     *
     * @param mobile 手机号
     * @return 是否合法
     */
    public static boolean isMobile(String mobile) {
        if (isEmpty(mobile)) {
            return false;
        }
        // 国际号码客户端传递过来格式：区号 + “-” + 号码
        if (!"".equals(mobile) && mobile.contains("-")) {
            return true;
        }
        Pattern p = Pattern.compile(SIMPLE_PHONE);
        Matcher m = p.matcher(mobile);
        return m.matches();
    }

    /**
     * 检测邮箱格式是否正确
     *
     * @param email email
     * @return 是否合法
     */
    public static boolean isValidEmail(String email) {
        if (isEmpty(email)) {
            return false;
        }
        boolean tag;
        final Pattern pattern = Pattern.compile(EMAIL);
        final Matcher mat = pattern.matcher(email);
        try {
            tag = mat.matches();
        } catch (Exception exception) {
            tag = false;
        }

        return tag;
    }

    /**
     * 首字母大写
     *
     * @param src 源字符串
     * @return 返回第一个大写后的字符串
     */
    public static String upperFirst(String src) {
        if (Character.isLowerCase(src.charAt(0))) {
            return 1 == src.length() ? src.toUpperCase() : Character.toUpperCase(src.charAt(0)) + src.substring(1);
        }
        return src;
    }

    /**
     * 首字母大写
     *
     * @param src 源字符串
     * @return 返回第一个大写后的字符串
     */
    public static String lowerFirst(String src) {
        if (Character.isUpperCase(src.charAt(0))) {
            return 1 == src.length() ? src.toLowerCase() : Character.toLowerCase(src.charAt(0)) + src.substring(1);
        }
        return src;
    }

    /**
     * 随机生成数字串
     *
     * @param len      Length
     * @param withZero contains Zero
     * @return Random Numbers
     */
    public static String getRandomNumber(int len, boolean withZero) {
        StringBuilder builder = new StringBuilder();
        int number;
        SecureRandom random = new SecureRandom();
        while (builder.length() < len) {
            number = random.nextInt(10);
            if (withZero) {
                builder.append(number);
            } else if (number > 0) {
                builder.append(number);
            }
        }
        return builder.toString();
    }

    /**
     * 随机生成字母串
     *
     * @param len Length
     * @return Random Letters
     */
    public static String getRandomLetter(int len) {
        StringBuilder str = new StringBuilder();
        SecureRandom random = new SecureRandom();
        for (int i = 0; i < len; i++) { // 你想生成几个字符的，就把len改成几．
            str.append(NumberUtil.to62RadixString(random.nextInt(63)));
        }
        return str.toString();
    }

    /**
     * @param len Length
     * @return Random Lower Case Letters
     * 随机生成小写字母串
     */
    public static String getRandomLowercaseLetter(int len) {
        return getRandomLetter(len, false);
    }

    /**
     * 随机生成大写字母串
     *
     * @param len Length
     * @return Random Upper Case Letters
     */
    public static String getRandomUppercaseLetter(int len) {
        return getRandomLetter(len, true);
    }

    private static String getRandomLetter(int len, boolean isUpper) {
        StringBuilder str = new StringBuilder();
        SecureRandom random = new SecureRandom();
        for (int i = 0; i < len; i++) { // 你想生成几个字符的，就把len改成几．
            str.append((char) (random.nextInt(26) + (isUpper ? 'A' : 'a')));
        }
        return str.toString();
    }

    /**
     * 随机字符
     *
     * @param count     字符数
     * @param uppercase 包含大写
     * @param lowercase 包含小写
     * @param number    包含数字
     * @return Random String
     */
    public static String getRandomText(int count, boolean uppercase, boolean lowercase, boolean number) {
        if (uppercase || lowercase || number) {
            SecureRandom random = new SecureRandom();
            StringBuilder builder = new StringBuilder();
            int value;
            while (builder.length() < count) {
                value = random.nextInt(26);
                if (number) {
                    builder.append((char) ('0' + value % 10));
                }
                if (uppercase) {
                    builder.append((char) ('A' + value));
                }
                if (lowercase) {
                    builder.append((char) ('a' + value));
                }
            }
            builder.setLength(count);
            return builder.toString();
        } else {
            throw new StatusException(StatusInfo.notSupported());
        }
    }

    /**
     * 删除html中引起安全问题的标签
     *
     * @param html Html
     * @return safe html
     */
    public static String safeHtml(String html) {
        String result = html;
        List<String> unSafeLables = new ArrayList<>();
        unSafeLables.add("<iframe[^>]*?>.*?</iframe>");
        unSafeLables.add("<frame[^>]*?>.*?</frame>");
        unSafeLables.add("<script[^>]*?>.*?</script>");
        unSafeLables.add("<title[^>]*?>.*?</title>");
        unSafeLables.add("<meta[^>]*?>");
        unSafeLables.add("<noscript[^>]*?>");
        unSafeLables.add("href=\"javascript:.*?\"");
        for (String lable : unSafeLables) {
            result = result.replaceAll(lable, "");
        }
        return result;
    }


    /**
     * <p>
     * Compares two CharSequences, returning <code>true</code> if they represent equal sequences of characters.
     * </p>
     * <p>
     * <code>null</code>s are handled without exceptions. Two <code>null</code> references are considered to be equal.
     * The comparison is case sensitive.
     * </p>
     * <pre>
     * StringUtil.equals(null, null)   = true
     * StringUtil.equals(null, "abc")  = false
     * StringUtil.equals("abc", null)  = false
     * StringUtil.equals("abc", "abc") = true
     * StringUtil.equals("abc", "ABC") = false
     * </pre>
     * <p>
     * Copied from Apache Commons Lang r1583482 on April 10, 2014 (day of 3.3.2 release).
     * </p>
     *
     * @param cs1 the first CharSequence, may be <code>null</code>
     * @param cs2 the second CharSequence, may be <code>null</code>
     * @return <code>true</code> if the CharSequences are equal (case-sensitive), or both <code>null</code>
     * @see Object#equals(Object)
     * @since 1.10
     */
    public static boolean equals(final CharSequence cs1, final CharSequence cs2) {
        if (cs1 == cs2) {
            return true;
        }
        if (cs1 == null || cs2 == null) {
            return false;
        }
        if (cs1 instanceof String && cs2 instanceof String) {
            return cs1.equals(cs2);
        }
        return regionMatches(cs1, false, 0, cs2, 0, Math.max(cs1.length(), cs2.length()));
    }

    /**
     * Green implementation of regionMatches.
     *
     * @param cs         the <code>CharSequence</code> to be processed
     * @param ignoreCase whether or not to be case insensitive
     * @param thisStart  the index to start on the <code>cs</code> CharSequence
     * @param substring  the <code>CharSequence</code> to be looked for
     * @param start      the index to start on the <code>substring</code> CharSequence
     * @param length     character length of the region
     * @return whether the region matched
     */
    private static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart,
                                         final CharSequence substring, final int start, final int length) {
        if (cs instanceof String && substring instanceof String) {
            return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length);
        }
        int index1 = thisStart;
        int index2 = start;
        int tmpLen = length;

        while (tmpLen-- > 0) {
            char c1 = cs.charAt(index1++);
            char c2 = substring.charAt(index2++);

            if (c1 == c2) {
                continue;
            }

            if (!ignoreCase) {
                return false;
            }

            // The same check as in String.regionMatches():
            if (Character.toUpperCase(c1) != Character.toUpperCase(c2)
                    && Character.toLowerCase(c1) != Character.toLowerCase(c2)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Calls {@link String#getBytes(Charset)}
     *
     * @param string  The string to encode (if null, return null).
     * @param charset The {@link Charset} to encode the <code>String</code>
     * @return the encoded bytes
     */
    private static byte[] getBytes(final String string, final Charset charset) {
        if (string == null) {
            return null;
        }
        return string.getBytes(charset);
    }

    /**
     * Encodes the given string into a sequence of bytes using the named charset, storing the result into a new byte
     * array.
     * <p>
     * This method catches {@link UnsupportedEncodingException} and rethrows it as {@link IllegalStateException}, which
     * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
     * </p>
     *
     * @param string      the String to encode, may be <code>null</code>
     * @param charsetName The name of a required {@link java.nio.charset.Charset}
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws IllegalStateException Thrown when a {@link UnsupportedEncodingException} is caught,
     *                               which should never happen for a required charset name.
     * @see String#getBytes(String)
     */
    public static byte[] getBytesUnchecked(final String string, final String charsetName) {
        if (string == null) {
            return null;
        }
        try {
            return string.getBytes(charsetName);
        } catch (final UnsupportedEncodingException e) {
            throw StringUtil.newIllegalStateException(charsetName, e);
        }
    }

    /**
     * Encodes the given string into a sequence of bytes using the US-ASCII charset, storing the result into a new byte
     * array.
     *
     * @param string the String to encode, may be <code>null</code>
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws NullPointerException Thrown if {@link StandardCharsets#US_ASCII} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
     * @see #getBytesUnchecked(String, String)
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static byte[] getBytesUsAscii(final String string) {
        return getBytes(string, StandardCharsets.US_ASCII);
    }

    /**
     * Encodes the given string into a sequence of bytes using the UTF-16 charset, storing the result into a new byte
     * array.
     *
     * @param string the String to encode, may be <code>null</code>
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
     * @see #getBytesUnchecked(String, String)
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static byte[] getBytesUtf16(final String string) {
        return getBytes(string, StandardCharsets.UTF_16);
    }

    /**
     * Encodes the given string into a sequence of bytes using the UTF-16BE charset, storing the result into a new byte
     * array.
     *
     * @param string the String to encode, may be <code>null</code>
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16BE} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
     * @see #getBytesUnchecked(String, String)
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static byte[] getBytesUtf16Be(final String string) {
        return getBytes(string, StandardCharsets.UTF_16BE);
    }

    /**
     * Encodes the given string into a sequence of bytes using the UTF-16LE charset, storing the result into a new byte
     * array.
     *
     * @param string the String to encode, may be <code>null</code>
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16LE} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
     * @see #getBytesUnchecked(String, String)
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static byte[] getBytesUtf16Le(final String string) {
        return getBytes(string, StandardCharsets.UTF_16LE);
    }

    /**
     * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte
     * array.
     *
     * @param string the String to encode, may be <code>null</code>
     * @return encoded bytes, or <code>null</code> if the input string was <code>null</code>
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_8} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
     * @see #getBytesUnchecked(String, String)
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static byte[] getBytesUtf8(final String string) {
        return getBytes(string, StandardCharsets.UTF_8);
    }

    private static IllegalStateException newIllegalStateException(final String charsetName,
                                                                  final UnsupportedEncodingException e) {
        return new IllegalStateException(charsetName + ": " + e);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the given charset.
     *
     * @param bytes   The bytes to be decoded into characters
     * @param charset The {@link Charset} to encode the <code>String</code>
     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_8} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     */
    private static String newString(final byte[] bytes, final Charset charset) {
        return bytes == null ? null : new String(bytes, charset);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the given charset.
     * <p>
     * This method catches {@link UnsupportedEncodingException} and re-throws it as {@link IllegalStateException}, which
     * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE.
     * </p>
     *
     * @param bytes       The bytes to be decoded into characters, may be <code>null</code>
     * @param charsetName The name of a required {@link java.nio.charset.Charset}
     * @return A new <code>String</code> decoded from the specified array of bytes using the given charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws IllegalStateException Thrown when a {@link UnsupportedEncodingException} is caught,
     *                               which should never happen for a required charset name.
     * @see String#String(byte[], String)
     */
    public static String newString(final byte[] bytes, final String charsetName) {
        if (bytes == null) {
            return null;
        }
        try {
            return new String(bytes, charsetName);
        } catch (final UnsupportedEncodingException e) {
            throw StringUtil.newIllegalStateException(charsetName, e);
        }
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the US-ASCII charset.
     *
     * @param bytes The bytes to be decoded into characters
     * @return A new <code>String</code> decoded from the specified array of bytes using the US-ASCII charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#US_ASCII} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static String newStringUsAscii(final byte[] bytes) {
        return new String(bytes, StandardCharsets.US_ASCII);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16 charset.
     *
     * @param bytes The bytes to be decoded into characters
     * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16 charset
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16} is not initialized,
     *                              which should never happen since it is required by the Java platform specification.
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static String newStringUtf16(final byte[] bytes) {
        return new String(bytes, StandardCharsets.UTF_16);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16BE charset.
     *
     * @param bytes The bytes to be decoded into characters
     * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16BE charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16BE} is not initialized, which should never happen since it is
     *                              required by the Java platform specification.
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static String newStringUtf16Be(final byte[] bytes) {
        return new String(bytes, StandardCharsets.UTF_16BE);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-16LE charset.
     *
     * @param bytes The bytes to be decoded into characters
     * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-16LE charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_16LE} is not initialized, which should never happen since it is
     *                              required by the Java platform specification.
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static String newStringUtf16Le(final byte[] bytes) {
        return new String(bytes, StandardCharsets.UTF_16LE);
    }

    /**
     * Constructs a new <code>String</code> by decoding the specified array of bytes using the UTF-8 charset.
     *
     * @param bytes The bytes to be decoded into characters
     * @return A new <code>String</code> decoded from the specified array of bytes using the UTF-8 charset,
     * or <code>null</code> if the input byte array was <code>null</code>.
     * @throws NullPointerException Thrown if {@link StandardCharsets#UTF_8} is not initialized, which should never happen since it is
     *                              required by the Java platform specification.
     * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException
     */
    public static String newStringUtf8(final byte[] bytes) {
        return newString(bytes, StandardCharsets.UTF_8);
    }

    /**
     * 非空字符串
     *
     * @param value        Value
     * @param defaultValue Default Value
     * @return 处理后字符串
     */
    public static String notBlank(String value, String defaultValue) {
        return isEmpty(value) ? defaultValue : value;
    }

    public static String hiddenPhone(String phone) {
        return hiddenPhone(phone, 4);
    }

    /**
     * 隐藏手机号码中间号码
     *
     * @param phone 手机号
     * @param count 隐藏位数
     * @return 处理后字符串
     */
    public static String hiddenPhone(String phone, int count) {
        if (phone == null) {
            return phone;
        }
        return hiddenMiddle(phone, count);
    }

    /**
     * 隐藏左边字符
     *
     * @param orgStr 原始字符串
     * @param count  隐藏位数
     * @return 处理后字符串
     */
    public static String hiddenLeft(String orgStr, int count) {
        return hidden(orgStr, 0, count);
    }

    /**
     * 隐藏右边字符
     *
     * @param orgStr 原始字符串
     * @param count  隐藏位数
     * @return 处理后字符串
     */
    public static String hiddenRight(String orgStr, int count) {
        if (orgStr == null) {
            return orgStr;
        }
        return hidden(orgStr, orgStr.length() - count, orgStr.length());
    }

    /**
     * 隐藏中间几位字符
     *
     * @param orgStr 原始串
     * @param count  隐藏位数
     * @return 处理后字符串
     */
    public static String hiddenMiddle(String orgStr, int count) {
        if (orgStr == null) {
            return orgStr;
        }
        int len = orgStr.length();
        if (len < (count + 2)) {
            return orgStr;
        }
        int start = (len - count) / 2;
        int end = start + count;
        return hidden(orgStr, start, end);
    }

    /**
     * 字符串隐藏
     *
     * @param orgStr 原始串
     * @param start  起始位置(包含,下标从0开始)
     * @param end    结束位置(不包含)
     * @return 处理后字符串
     */
    public static String hidden(String orgStr, int start, int end) {
        if (orgStr == null) {
            return orgStr;
        }
        int len = orgStr.length();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < len; i++) {
            if (i >= start && i < end) {
                sb.append("*");
            } else {
                sb.append(orgStr.charAt(i));
            }
        }
        return sb.toString();
    }

    /**
     * 检测字符串中是否包含emoji或者其他非文字类型的字符
     *
     * @param value value
     * @return 是否包含表情符号
     */
    public static boolean containsEmoji(String value) {
        return EmojiUtil.containsEmoji(value);
    }

    /**
     * 过滤emoji 或者 其他非文字类型的字符
     *
     * @param value value
     * @return 过滤表情符号
     */
    public static String filterEmoji(String value) {
        if (isBlank(value)) {
            return value;
        }
        return EmojiUtil.removeAllEmojis(value);
    }

    /**
     * 检测字符串中是否有特殊符号
     *
     * @param value value
     * @return 是否包含特殊字符号
     */
    public static boolean checkSymbol(String value) {
        Pattern p = Pattern.compile(SYMBOL);
        Matcher m = p.matcher(value);
        return m.find();
    }

    /**
     * 过滤字符串中所有的特殊符号
     *
     * @param source 需要过滤的字符串
     * @return String
     */
    public static String filterSymbol(String source) {
        if (isEmpty(source)) {
            return "";
        }
        source = source.replaceAll(" ", "");
        Pattern p = Pattern.compile(SYMBOL);
        Matcher m = p.matcher(source);
        return m.replaceAll("").trim();
    }


    /**
     * <pre>
     * 检查版本号
     * 版本号格式 x.x.x.x.x.x  (x只能用数字表示)
     * 左边版本号大于右边版本号 用 正数表示
     * 左边版本号小于右边版本号 用 负数表示
     * 两边版本号相同 用0表示
     * </pre>
     *
     * @param v1 v1
     * @param v2 v2
     * @return 比较结果
     */
    public static int compareVersion(String v1, String v2) {
        if (StringUtil.isEmpty(v1) && StringUtil.isEmpty(v2)) {
            return 0;
        }
        if (StringUtil.isEmpty(v1)) {
            return 1;
        } else if (StringUtil.isEmpty(v2)) {
            return -1;
        } else {
            String[] part1 = v1.split("\\.");
            String[] part2 = v2.split("\\.");
            int len1 = part1.length;
            int len2 = part2.length;
            int len = Math.min(len1, len2);
            for (int i = 0; i < len; i++) {
                int n1 = NumberUtil.getInteger(part1[i], 0);
                int n2 = NumberUtil.getInteger(part2[i], 0);
                if (n1 != n2) {
                    return n1 - n2;
                }
            }
            return len1 - len2;
        }
    }

    // Replacing
    //-----------------------------------------------------------------------

    /**
     * <p>Replaces a String with another String inside a larger String, once.</p>
     *
     * <p>A <code>null</code> reference passed to this method is a no-op.</p>
     *
     * <pre>
     * StringUtil.replaceOnce(null, *, *)        = null
     * StringUtil.replaceOnce("", *, *)          = ""
     * StringUtil.replaceOnce("any", null, *)    = "any"
     * StringUtil.replaceOnce("any", *, null)    = "any"
     * StringUtil.replaceOnce("any", "", *)      = "any"
     * StringUtil.replaceOnce("aba", "a", null)  = "aba"
     * StringUtil.replaceOnce("aba", "a", "")    = "ba"
     * StringUtil.replaceOnce("aba", "a", "z")   = "zba"
     * </pre>
     *
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for, may be null
     * @param replacement  the String to replace with, may be null
     * @return the text with any replacements processed,
     * <code>null</code> if null String input
     * @see #replace(String text, String searchString, String replacement, int max)
     */
    public static String replaceOnce(String text, String searchString, String replacement) {
        return replace(text, searchString, replacement, 1);
    }

    /**
     * <p>Replaces all occurrences of a String within another String.</p>
     *
     * <p>A <code>null</code> reference passed to this method is a no-op.</p>
     *
     * <pre>
     * StringUtil.replace(null, *, *)        = null
     * StringUtil.replace("", *, *)          = ""
     * StringUtil.replace("any", null, *)    = "any"
     * StringUtil.replace("any", *, null)    = "any"
     * StringUtil.replace("any", "", *)      = "any"
     * StringUtil.replace("aba", "a", null)  = "aba"
     * StringUtil.replace("aba", "a", "")    = "b"
     * StringUtil.replace("aba", "a", "z")   = "zbz"
     * </pre>
     *
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for, may be null
     * @param replacement  the String to replace it with, may be null
     * @return the text with any replacements processed,
     * <code>null</code> if null String input
     * @see #replace(String text, String searchString, String replacement, int max)
     */
    public static String replace(String text, String searchString, String replacement) {
        return replace(text, searchString, replacement, -1);
    }

    /**
     * <p>Replaces a String with another String inside a larger String,
     * for the first <code>max</code> values of the search String.</p>
     *
     * <p>A <code>null</code> reference passed to this method is a no-op.</p>
     *
     * <pre>
     * StringUtil.replace(null, *, *, *)         = null
     * StringUtil.replace("", *, *, *)           = ""
     * StringUtil.replace("any", null, *, *)     = "any"
     * StringUtil.replace("any", *, null, *)     = "any"
     * StringUtil.replace("any", "", *, *)       = "any"
     * StringUtil.replace("any", *, *, 0)        = "any"
     * StringUtil.replace("abaa", "a", null, -1) = "abaa"
     * StringUtil.replace("abaa", "a", "", -1)   = "b"
     * StringUtil.replace("abaa", "a", "z", 0)   = "abaa"
     * StringUtil.replace("abaa", "a", "z", 1)   = "zbaa"
     * StringUtil.replace("abaa", "a", "z", 2)   = "zbza"
     * StringUtil.replace("abaa", "a", "z", -1)  = "zbzz"
     * </pre>
     *
     * @param text         text to search and replace in, may be null
     * @param searchString the String to search for, may be null
     * @param replacement  the String to replace it with, may be null
     * @param max          maximum number of values to replace, or <code>-1</code> if no maximum
     * @return the text with any replacements processed,
     * <code>null</code> if null String input
     */
    public static String replace(String text, String searchString, String replacement, int max) {
        if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) {
            return text;
        }
        int start = 0;
        int end = text.indexOf(searchString, start);
        if (end == INDEX_NOT_FOUND) {
            return text;
        }
        int replLength = searchString.length();
        int increase = replacement.length() - replLength;
        increase = Math.max(increase, 0);
        increase *= (max < 0 ? 16 : Math.min(max, 64));
        StringBuilder buf = new StringBuilder(text.length() + increase);
        while (end != INDEX_NOT_FOUND) {
            buf.append(text.substring(start, end)).append(replacement);
            start = end + replLength;
            if (--max == 0) {
                break;
            }
            end = text.indexOf(searchString, start);
        }
        buf.append(text.substring(start));
        return buf.toString();
    }

    /**
     * <p>
     * Replaces all occurrences of Strings within another String.
     * </p>
     *
     * <p>
     * A <code>null</code> reference passed to this method is a no-op, or if
     * any "search string" or "string to replace" is null, that replace will be
     * ignored. This will not repeat. For repeating replaces, call the
     * overloaded method.
     * </p>
     *
     * <pre>
     *  StringUtil.replaceEach(null, *, *)        = null
     *  StringUtil.replaceEach("", *, *)          = ""
     *  StringUtil.replaceEach("aba", null, null) = "aba"
     *  StringUtil.replaceEach("aba", new String[0], null) = "aba"
     *  StringUtil.replaceEach("aba", null, new String[0]) = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, null)  = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, new String[]{""})  = "b"
     *  StringUtil.replaceEach("aba", new String[]{null}, new String[]{"a"})  = "aba"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"})  = "wcte"
     *  (example of how it does not repeat)
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"})  = "dcte"
     * </pre>
     *
     * @param text            text to search and replace in, no-op if null
     * @param searchList      the Strings to search for, no-op if null
     * @param replacementList the Strings to replace them with, no-op if null
     * @return the text with any replacements processed, <code>null</code> if
     * null String input
     * @throws IndexOutOfBoundsException if the lengths of the arrays are not the same (null is ok,
     *                                   and/or size 0)
     * @since 2.4
     */
    public static String replaceEach(String text, String[] searchList, String[] replacementList) {
        return replaceEach(text, searchList, replacementList, false, 0);
    }

    /**
     * <p>
     * Replaces all occurrences of Strings within another String.
     * </p>
     *
     * <p>
     * A <code>null</code> reference passed to this method is a no-op, or if
     * any "search string" or "string to replace" is null, that replace will be
     * ignored. This will not repeat. For repeating replaces, call the
     * overloaded method.
     * </p>
     *
     * <pre>
     *  StringUtil.replaceEach(null, *, *, *) = null
     *  StringUtil.replaceEach("", *, *, *) = ""
     *  StringUtil.replaceEach("aba", null, null, *) = "aba"
     *  StringUtil.replaceEach("aba", new String[0], null, *) = "aba"
     *  StringUtil.replaceEach("aba", null, new String[0], *) = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
     *  StringUtil.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
     *  (example of how it repeats)
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, true) = IllegalArgumentException
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, false) = "dcabe"
     * </pre>
     *
     * @param text            text to search and replace in, no-op if null
     * @param searchList      the Strings to search for, no-op if null
     * @param replacementList the Strings to replace them with, no-op if null
     * @return the text with any replacements processed, <code>null</code> if
     * null String input
     * @throws IllegalArgumentException  if the search is repeating and there is an endless loop due
     *                                   to outputs of one being inputs to another
     * @throws IndexOutOfBoundsException if the lengths of the arrays are not the same (null is ok,
     *                                   and/or size 0)
     * @since 2.4
     */
    public static String replaceEachRepeatedly(String text, String[] searchList, String[] replacementList) {
        // timeToLive should be 0 if not used or nothing to replace, else it's
        // the length of the replace array
        int timeToLive = searchList == null ? 0 : searchList.length;
        return replaceEach(text, searchList, replacementList, true, timeToLive);
    }

    /**
     * <p>
     * Replaces all occurrences of Strings within another String.
     * </p>
     *
     * <p>
     * A <code>null</code> reference passed to this method is a no-op, or if
     * any "search string" or "string to replace" is null, that replace will be
     * ignored.
     * </p>
     *
     * <pre>
     *  StringUtil.replaceEach(null, *, *, *) = null
     *  StringUtil.replaceEach("", *, *, *) = ""
     *  StringUtil.replaceEach("aba", null, null, *) = "aba"
     *  StringUtil.replaceEach("aba", new String[0], null, *) = "aba"
     *  StringUtil.replaceEach("aba", null, new String[0], *) = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, null, *) = "aba"
     *  StringUtil.replaceEach("aba", new String[]{"a"}, new String[]{""}, *) = "b"
     *  StringUtil.replaceEach("aba", new String[]{null}, new String[]{"a"}, *) = "aba"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"w", "t"}, *) = "wcte"
     *  (example of how it repeats)
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, false) = "dcte"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "t"}, true) = "tcte"
     *  StringUtil.replaceEach("abcde", new String[]{"ab", "d"}, new String[]{"d", "ab"}, *) = IllegalArgumentException
     * </pre>
     *
     * @param text            text to search and replace in, no-op if null
     * @param searchList      the Strings to search for, no-op if null
     * @param replacementList the Strings to replace them with, no-op if null
     * @param repeat          if true, then replace repeatedly
     *                        until there are no more possible replacements or timeToLive < 0
     * @param timeToLive      if less than 0 then there is a circular reference and endless
     *                        loop
     * @return the text with any replacements processed, <code>null</code> if null String input
     * @throws IllegalArgumentException  if the search is repeating and there is an endless loop due
     *                                   to outputs of one being inputs to another
     * @throws IndexOutOfBoundsException if the lengths of the arrays are not the same (null is ok,
     *                                   and/or size 0)
     * @since 2.4
     */
    private static String replaceEach(String text, String[] searchList, String[] replacementList,
                                      boolean repeat, int timeToLive) {

        // mchyzer Performance note: This creates very few new objects (one major goal)
        // let me know if there are performance requests, we can create a harness to measure

        if (text == null || text.length() == 0 || searchList == null
                || searchList.length == 0 || replacementList == null || replacementList.length == 0) {
            return text;
        }

        // if recursing, this shouldnt be less than 0
        if (timeToLive < 0) {
            throw new IllegalStateException("TimeToLive of " + timeToLive + " is less than 0: " + text);
        }

        int searchLength = searchList.length;
        int replacementLength = replacementList.length;

        // make sure lengths are ok, these need to be equal
        if (searchLength != replacementLength) {
            throw new IllegalArgumentException("Search and Replace array lengths don't match: "
                    + searchLength
                    + " vs "
                    + replacementLength);
        }

        // keep track of which still have matches
        boolean[] noMoreMatchesForReplIndex = new boolean[searchLength];

        // index on index that the match was found
        int textIndex = -1;
        int replaceIndex = -1;
        int tempIndex;

        // index of replace array that will replace the search string found
        // NOTE: logic duplicated below START
        for (int i = 0; i < searchLength; i++) {
            if (noMoreMatchesForReplIndex[i] || searchList[i] == null
                    || searchList[i].length() == 0 || replacementList[i] == null) {
                continue;
            }
            tempIndex = text.indexOf(searchList[i]);

            // see if we need to keep searching for this
            if (tempIndex == -1) {
                noMoreMatchesForReplIndex[i] = true;
            } else {
                if (textIndex == -1 || tempIndex < textIndex) {
                    textIndex = tempIndex;
                    replaceIndex = i;
                }
            }
        }
        // NOTE: logic mostly below END

        // no search strings found, we are done
        if (textIndex == -1) {
            return text;
        }

        int start = 0;

        // get a good guess on the size of the result buffer so it doesnt have to double if it goes over a bit
        int increase = 0;

        // count the replacement text elements that are larger than their corresponding text being replaced
        for (int i = 0; i < searchList.length; i++) {
            if (searchList[i] == null || replacementList[i] == null) {
                continue;
            }
            int greater = replacementList[i].length() - searchList[i].length();
            if (greater > 0) {
                increase += 3 * greater; // assume 3 matches
            }
        }
        // have upper-bound at 20% increase, then let Java take over
        increase = Math.min(increase, text.length() / 5);

        StringBuilder buf = new StringBuilder(text.length() + increase);

        while (textIndex != -1) {

            for (int i = start; i < textIndex; i++) {
                buf.append(text.charAt(i));
            }
            buf.append(replacementList[replaceIndex]);

            start = textIndex + searchList[replaceIndex].length();

            textIndex = -1;
            replaceIndex = -1;
            // find the next earliest match
            // NOTE: logic mostly duplicated above START
            for (int i = 0; i < searchLength; i++) {
                if (noMoreMatchesForReplIndex[i] || searchList[i] == null
                        || searchList[i].length() == 0 || replacementList[i] == null) {
                    continue;
                }
                tempIndex = text.indexOf(searchList[i], start);

                // see if we need to keep searching for this
                if (tempIndex == -1) {
                    noMoreMatchesForReplIndex[i] = true;
                } else {
                    if (textIndex == -1 || tempIndex < textIndex) {
                        textIndex = tempIndex;
                        replaceIndex = i;
                    }
                }
            }
            // NOTE: logic duplicated above END

        }
        int textLength = text.length();
        for (int i = start; i < textLength; i++) {
            buf.append(text.charAt(i));
        }
        String result = buf.toString();
        if (!repeat) {
            return result;
        }

        return replaceEach(result, searchList, replacementList, repeat, timeToLive - 1);
    }

    // Replace, character based
    //-----------------------------------------------------------------------

    /**
     * <p>Replaces all occurrences of a character in a String with another.
     * This is a null-safe version of {@link String#replace(char, char)}.</p>
     *
     * <p>A <code>null</code> string input returns <code>null</code>.
     * An empty ("") string input returns an empty string.</p>
     *
     * <pre>
     * StringUtil.replaceChars(null, *, *)        = null
     * StringUtil.replaceChars("", *, *)          = ""
     * StringUtil.replaceChars("abcba", 'b', 'y') = "aycya"
     * StringUtil.replaceChars("abcba", 'z', 'y') = "abcba"
     * </pre>
     *
     * @param str         String to replace characters in, may be null
     * @param searchChar  the character to search for, may be null
     * @param replaceChar the character to replace, may be null
     * @return modified String, <code>null</code> if null string input
     * @since 2.0
     */
    public static String replaceChars(String str, char searchChar, char replaceChar) {
        if (str == null) {
            return null;
        }
        return str.replace(searchChar, replaceChar);
    }

    /**
     * <p>Replaces multiple characters in a String in one go.
     * This method can also be used to delete characters.</p>
     *
     * <p>For example:
     * <code>replaceChars(&quot;hello&quot;, &quot;ho&quot;, &quot;jy&quot;) = jelly</code>.</p>
     *
     * <p>A <code>null</code> string input returns <code>null</code>.
     * An empty ("") string input returns an empty string.
     * A null or empty set of search characters returns the input string.</p>
     *
     * <p>The length of the search characters should normally equal the length
     * of the replace characters.
     * If the search characters is longer, then the extra search characters
     * are deleted.
     * If the search characters is shorter, then the extra replace characters
     * are ignored.</p>
     *
     * <pre>
     * StringUtil.replaceChars(null, *, *)           = null
     * StringUtil.replaceChars("", *, *)             = ""
     * StringUtil.replaceChars("abc", null, *)       = "abc"
     * StringUtil.replaceChars("abc", "", *)         = "abc"
     * StringUtil.replaceChars("abc", "b", null)     = "ac"
     * StringUtil.replaceChars("abc", "b", "")       = "ac"
     * StringUtil.replaceChars("abcba", "bc", "yz")  = "ayzya"
     * StringUtil.replaceChars("abcba", "bc", "y")   = "ayya"
     * StringUtil.replaceChars("abcba", "bc", "yzx") = "ayzya"
     * </pre>
     *
     * @param str          String to replace characters in, may be null
     * @param searchChars  a set of characters to search for, may be null
     * @param replaceChars a set of characters to replace, may be null
     * @return modified String, <code>null</code> if null string input
     * @since 2.0
     */
    public static String replaceChars(String str, String searchChars, String replaceChars) {
        if (isEmpty(str) || isEmpty(searchChars)) {
            return str;
        }
        if (replaceChars == null) {
            replaceChars = EMPTY;
        }
        boolean modified = false;
        int replaceCharsLength = replaceChars.length();
        int strLength = str.length();
        StringBuilder buf = new StringBuilder(strLength);
        for (int i = 0; i < strLength; i++) {
            char ch = str.charAt(i);
            int index = searchChars.indexOf(ch);
            if (index >= 0) {
                modified = true;
                if (index < replaceCharsLength) {
                    buf.append(replaceChars.charAt(index));
                }
            } else {
                buf.append(ch);
            }
        }
        if (modified) {
            return buf.toString();
        }
        return str;
    }

    // Joining
    //-----------------------------------------------------------------------

    /**
     * <p>Joins the elements of the provided array into a single String
     * containing the provided list of elements.</p>
     *
     * <p>No separator is added to the joined String.
     * Null objects or empty strings within the array are represented by
     * empty strings.</p>
     *
     * <pre>
     * StringUtil.join(null)            = null
     * StringUtil.join([])              = ""
     * StringUtil.join([null])          = ""
     * StringUtil.join(["a", "b", "c"]) = "abc"
     * StringUtil.join([null, "", "a"]) = "a"
     * </pre>
     *
     * @param array the array of values to join together, may be null
     * @return the joined String, <code>null</code> if null array input
     */
    public static String join(Object[] array) {
        return join(array, null);
    }

    /**
     * <p>Joins the elements of the provided array into a single String
     * containing the provided list of elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * Null objects or empty strings within the array are represented by
     * empty strings.</p>
     *
     * <pre>
     * StringUtil.join(null, *)               = null
     * StringUtil.join([], *)                 = ""
     * StringUtil.join([null], *)             = ""
     * StringUtil.join(["a", "b", "c"], ';')  = "a;b;c"
     * StringUtil.join(["a", "b", "c"], null) = "abc"
     * StringUtil.join([null, "", "a"], ';')  = ";;a"
     * </pre>
     *
     * @param array     the array of values to join together, may be null
     * @param separator the separator character to use
     * @return the joined String, <code>null</code> if null array input
     * @since 2.0
     */
    public static String join(Object[] array, char separator) {
        if (array == null) {
            return null;
        }

        return join(array, separator, 0, array.length);
    }

    /**
     * <p>Joins the elements of the provided array into a single String
     * containing the provided list of elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * Null objects or empty strings within the array are represented by
     * empty strings.</p>
     *
     * <pre>
     * StringUtil.join(null, *)               = null
     * StringUtil.join([], *)                 = ""
     * StringUtil.join([null], *)             = ""
     * StringUtil.join(["a", "b", "c"], ';')  = "a;b;c"
     * StringUtil.join(["a", "b", "c"], null) = "abc"
     * StringUtil.join([null, "", "a"], ';')  = ";;a"
     * </pre>
     *
     * @param array      the array of values to join together, may be null
     * @param separator  the separator character to use
     * @param startIndex the first index to start joining from.  It is
     *                   an error to pass in an end index past the end of the array
     * @param endIndex   the index to stop joining from (exclusive). It is
     *                   an error to pass in an end index past the end of the array
     * @return the joined String, <code>null</code> if null array input
     * @since 2.0
     */
    public static String join(Object[] array, char separator, int startIndex, int endIndex) {
        if (array == null) {
            return null;
        }
        int bufSize = (endIndex - startIndex);
        if (bufSize <= 0) {
            return EMPTY;
        }

        bufSize *= ((array[startIndex] == null ? 16 : array[startIndex].toString().length()) + 1);
        StringBuilder buf = new StringBuilder(bufSize);

        for (int i = startIndex; i < endIndex; i++) {
            if (i > startIndex) {
                buf.append(separator);
            }
            if (array[i] != null) {
                buf.append(array[i]);
            }
        }
        return buf.toString();
    }


    /**
     * <p>Joins the elements of the provided array into a single String
     * containing the provided list of elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * A <code>null</code> separator is the same as an empty String ("").
     * Null objects or empty strings within the array are represented by
     * empty strings.</p>
     *
     * <pre>
     * StringUtil.join(null, *)                = null
     * StringUtil.join([], *)                  = ""
     * StringUtil.join([null], *)              = ""
     * StringUtil.join(["a", "b", "c"], "--")  = "a--b--c"
     * StringUtil.join(["a", "b", "c"], null)  = "abc"
     * StringUtil.join(["a", "b", "c"], "")    = "abc"
     * StringUtil.join([null, "", "a"], ',')   = ",,a"
     * </pre>
     *
     * @param array     the array of values to join together, may be null
     * @param separator the separator character to use, null treated as ""
     * @return the joined String, <code>null</code> if null array input
     */
    public static String join(Object[] array, String separator) {
        if (array == null) {
            return null;
        }
        return join(array, separator, 0, array.length);
    }

    /**
     * <p>Joins the elements of the provided array into a single String
     * containing the provided list of elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * A <code>null</code> separator is the same as an empty String ("").
     * Null objects or empty strings within the array are represented by
     * empty strings.</p>
     *
     * <pre>
     * StringUtil.join(null, *)                = null
     * StringUtil.join([], *)                  = ""
     * StringUtil.join([null], *)              = ""
     * StringUtil.join(["a", "b", "c"], "--")  = "a--b--c"
     * StringUtil.join(["a", "b", "c"], null)  = "abc"
     * StringUtil.join(["a", "b", "c"], "")    = "abc"
     * StringUtil.join([null, "", "a"], ',')   = ",,a"
     * </pre>
     *
     * @param array      the array of values to join together, may be null
     * @param separator  the separator character to use, null treated as ""
     * @param startIndex the first index to start joining from.  It is
     *                   an error to pass in an end index past the end of the array
     * @param endIndex   the index to stop joining from (exclusive). It is
     *                   an error to pass in an end index past the end of the array
     * @return the joined String, <code>null</code> if null array input
     */
    public static String join(Object[] array, String separator, int startIndex, int endIndex) {
        if (array == null) {
            return null;
        }
        if (separator == null) {
            separator = EMPTY;
        }

        // endIndex - startIndex > 0:   Len = NofStrings *(len(firstString) + len(separator))
        //           (Assuming that all Strings are roughly equally long)
        int bufSize = (endIndex - startIndex);
        if (bufSize <= 0) {
            return EMPTY;
        }

        bufSize *= ((array[startIndex] == null ? 16 : array[startIndex].toString().length())
                + separator.length());

        StringBuilder buf = new StringBuilder(bufSize);

        for (int i = startIndex; i < endIndex; i++) {
            if (i > startIndex) {
                buf.append(separator);
            }
            if (array[i] != null) {
                buf.append(array[i]);
            }
        }
        return buf.toString();
    }

    /**
     * <p>Joins the elements of the provided <code>Iterator</code> into
     * a single String containing the provided elements.</p>
     *
     * <p>No delimiter is added before or after the list. Null objects or empty
     * strings within the iteration are represented by empty strings.</p>
     *
     * <p>See the examples here: {@link #join(Object[], char)}. </p>
     *
     * @param iterator  the <code>Iterator</code> of values to join together, may be null
     * @param separator the separator character to use
     * @return the joined String, <code>null</code> if null iterator input
     * @since 2.0
     */
    public static String join(Iterator<?> iterator, char separator) {

        // handle null, zero and one elements before building a buffer
        if (iterator == null) {
            return null;
        }
        if (!iterator.hasNext()) {
            return EMPTY;
        }
        Object first = iterator.next();
        if (!iterator.hasNext()) {
            return ObjectUtil.toString(first);
        }

        // two or more elements
        StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small
        if (first != null) {
            buf.append(first);
        }

        while (iterator.hasNext()) {
            buf.append(separator);
            Object obj = iterator.next();
            if (obj != null) {
                buf.append(obj);
            }
        }

        return buf.toString();
    }

    /**
     * <p>Joins the elements of the provided <code>Iterator</code> into
     * a single String containing the provided elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * A <code>null</code> separator is the same as an empty String ("").</p>
     *
     * <p>See the examples here: {@link #join(Object[], String)}. </p>
     *
     * @param iterator  the <code>Iterator</code> of values to join together, may be null
     * @param separator the separator character to use, null treated as ""
     * @return the joined String, <code>null</code> if null iterator input
     */
    public static String join(Iterator<?> iterator, String separator) {

        // handle null, zero and one elements before building a buffer
        if (iterator == null) {
            return null;
        }
        if (!iterator.hasNext()) {
            return EMPTY;
        }
        Object first = iterator.next();
        if (!iterator.hasNext()) {
            return ObjectUtil.toString(first);
        }

        // two or more elements
        StringBuilder buf = new StringBuilder(256); // Java default is 16, probably too small
        if (first != null) {
            buf.append(first);
        }

        while (iterator.hasNext()) {
            if (separator != null) {
                buf.append(separator);
            }
            Object obj = iterator.next();
            if (obj != null) {
                buf.append(obj);
            }
        }
        return buf.toString();
    }

    /**
     * <p>Joins the elements of the provided <code>Collection</code> into
     * a single String containing the provided elements.</p>
     *
     * <p>No delimiter is added before or after the list. Null objects or empty
     * strings within the iteration are represented by empty strings.</p>
     *
     * <p>See the examples here: {@link #join(Object[], char)}. </p>
     *
     * @param collection the <code>Collection</code> of values to join together, may be null
     * @param separator  the separator character to use
     * @return the joined String, <code>null</code> if null iterator input
     * @since 2.3
     */
    public static String join(Collection<?> collection, char separator) {
        if (collection == null) {
            return null;
        }
        return join(collection.iterator(), separator);
    }

    /**
     * <p>Joins the elements of the provided <code>Collection</code> into
     * a single String containing the provided elements.</p>
     *
     * <p>No delimiter is added before or after the list.
     * A <code>null</code> separator is the same as an empty String ("").</p>
     *
     * <p>See the examples here: {@link #join(Object[], String)}. </p>
     *
     * @param collection the <code>Collection</code> of values to join together, may be null
     * @param separator  the separator character to use, null treated as ""
     * @return the joined String, <code>null</code> if null iterator input
     * @since 2.3
     */
    public static String join(Collection<?> collection, String separator) {
        if (collection == null) {
            return null;
        }
        return join(collection.iterator(), separator);
    }

}
